package cms.api.solr.index.service;

import cms.api.framework.search.IndexService;
import cms.api.framework.search.Indexable;
import cms.api.framework.search.ListQueryParams;
import cms.api.framework.search.PageQueryParams;
import cms.api.framework.search.QueryParams;
import cms.api.framework.search.SearchResultPage;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.request.schema.SchemaRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.client.solrj.response.schema.SchemaResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Solr索引服务实现类
 *
 * @author heyingcheng
 * @email heyingcheng@ctvit.com.cn
 * @date 2019/6/29 18:00
 */
@Slf4j
public abstract class BaseSolrServiceImpl extends BaseIndexServiceImpl implements IndexService {

    /** 冒号 */
    public static final String COLON = ":";
    /** 逗号 */
    public static final String COMMA = ",";
    /** AND连接符 */
    public static final String AND = " AND ";
    /** Field名称 */
    public static final String NAME = "name";
    /** Field类型 */
    public static final String TYPE = "type";
    /** Solr每页最大记录数 */
    public static final int MAX_ROWS = 5000;
    /** Solr客户端 */
    @Autowired
    protected SolrClient solrClient;

    @Override
    public boolean isTypeExist(String index, String type) {
        log.info("判断索引库中的文档类型是否已存在, index=[{}], type=[{}]", index, type);
        return false;
    }

    @Override
    public String addDocument(String index, String type, Indexable document) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        String docId = document.getDocId();
        Map documentObject = document.getDocumentObject();
        try {
            if (StringUtils.isNotBlank(docId)) {
                if (isDocumentExist(index, docId)) {
                    return docId;
                }
            } else {
                docId = this.generateDocId(getGeneratorKey4Index(index));
                documentObject.put(document.getDocIdFieldName(), docId);
            }
            addOrUpdateDocument(index, documentObject);
            solrClient.commit(index);
            return docId;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], document=[{}]", index, type, document.getDocumentObject(), e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], document=[{}]", index, type, document.getDocumentObject(), e);
        }
        return StringUtils.EMPTY;
    }

    /**
     * 从索引库中删除文档数据
     *
     * <pre>
     *     Solr无法返回被删除的文档数目，返回0表示操作失败，返回1表示操作成功
     * </pre>
     *
     * @param index 索引库名称
     * @param type  文档类型名称
     * @param id    文档ID
     * @return long
     * @author heyingcheng
     * @date 2019/7/18 23:50
     */
    @Override
    public long deleteDocumentById(String index, String type, String id) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        if (StringUtils.isBlank(id)) {
            throw new IllegalArgumentException("id cannot be blank");
        }

        try {
            UpdateResponse updateResponse = solrClient.deleteById(index, id);
            solrClient.commit(index);
            JSONObject responseHeaderObject = this.formatNamedList(updateResponse.getResponseHeader());
            Integer status = responseHeaderObject.getInteger("status");
            if (Objects.nonNull(status) && status.equals(NumberUtils.INTEGER_ZERO)) {
                return NumberUtils.LONG_ONE;
            }
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], id=[{}]", index, type, id, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], id=[{}]", index, type, id, e);
        }
        return NumberUtils.LONG_ZERO;
    }

    /**
     * 从索引库中删除查询到的文档数据
     *
     * <pre>
     *     Solr无法返回被删除的文档数目，返回0表示操作失败，返回1表示操作成功
     * </pre>
     *
     * @param index       索引库名称
     * @param type        文档类型名称
     * @param queryParams 查询参数对象
     * @return long
     * @author heyingcheng
     * @date 2019/7/17 10:47
     */
    @Override
    public long deleteDocumentByQuery(String index, String type, QueryParams queryParams) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        Map<String, Object> conditions = queryParams.getConditions();
        if (MapUtils.isEmpty(conditions)) {
            throw new IllegalArgumentException("QueryParams.conditions cannot be empty");
        }

        try {
            UpdateResponse updateResponse = solrClient.deleteByQuery(index, convertConditionsToString(conditions));
            solrClient.commit(index);
            JSONObject responseHeaderObject = this.formatNamedList(updateResponse.getResponseHeader());
            Integer status = responseHeaderObject.getInteger("status");
            if (Objects.nonNull(status) && status.equals(NumberUtils.INTEGER_ZERO)) {
                return NumberUtils.LONG_ONE;
            }
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], queryParams=[{}]", index, type, JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], queryParams=[{}]", index, type, JSON.toJSONString(queryParams), e);
        }
        return NumberUtils.LONG_ZERO;
    }

    /**
     * 更新索引库中的文档数据
     *
     * <pre>
     *     Solr无法返回被更新/插入的文档数，返回0表示操作失败，返回1表示操作成功
     * </pre>
     *
     * @param index    索引库名称
     * @param type     文档类型名称
     * @param document 文档数据
     * @param isUpsert 指示当索引库中如果不存在该文档是否需要插入 true=插入 false=不插入，默认为true
     * @return long
     * @author heyingcheng
     * @date 2019/7/19 10:57
     */
    @Override
    public long updateDocument(String index, String type, Indexable document, boolean isUpsert) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        String docId = document.getDocId();
        Map documentObject = document.getDocumentObject();
        try {
            if (isUpsert) {
                if (StringUtils.isBlank(docId)) {
                    docId = this.generateDocId(getGeneratorKey4Index(index));
                    documentObject.put(document.getDocIdFieldName(), docId);
                }
            } else if (StringUtils.isBlank(docId)) {
                return NumberUtils.LONG_ZERO;
            }
            addOrUpdateDocument(index, documentObject);
            solrClient.commit(index);
            return NumberUtils.LONG_ONE;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], document=[{}], isUpsert=[{}]", index, type, JSON.toJSONString(document), isUpsert, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], document=[{}], isUpsert=[{}]", index, type, JSON.toJSONString(document), isUpsert, e);
        }
        return NumberUtils.INTEGER_ZERO;
    }

    @Override
    public Map<String, Object> fetchDocumentById(String index, String type, String id, String... rtnFields) {
        try {
            return fetchDocumentById(index, id, (solrQuery) -> {
                solrQuery.setFields(rtnFields);
            });
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], id=[{}], rtnFields=[{}]", index, type, id, rtnFields, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], id=[{}], rtnFields=[{}]", index, type, id, rtnFields, e);
        }
        return Collections.emptyMap();
    }

    @Override
    public SearchResultPage search(PageQueryParams queryParams) {
        int startPage = queryParams.getStartPage();
        int pageSize = queryParams.getPageSize();

        if (startPage == NumberUtils.INTEGER_ZERO.intValue()) {
            startPage = NumberUtils.INTEGER_ONE.intValue();
        }

        try {
            SearchResultPage searchResultPage = searchForResultPage(queryParams, startPage, pageSize);
            return searchResultPage;
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        }
        return new SearchResultPage(startPage, pageSize, NumberUtils.INTEGER_ZERO, Collections.emptyList());
    }

    @Override
    public List<Map<String, Object>> search(ListQueryParams queryParams) {
        try {
            SearchResultPage searchResultPage = searchForResultPage(queryParams, NumberUtils.INTEGER_ONE.intValue(), MAX_ROWS);
            return searchResultPage.getRecordList();
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        } catch (IOException e) {
            log.error("IO异常, queryParams=[{}]", JSON.toJSONString(queryParams), e);
        }
        return Collections.emptyList();
    }

    /**
     * 设置文档类型的字段类型定义
     * <pre>
     *     mapping格式为key1,value1,key2,value2
     * </pre>
     *
     * @param index   索引库名称
     * @param type    文档类型名称
     * @param mapping 类型定义字符串
     * @return void
     * @author heyingcheng
     * @date 2019/7/19 1:49
     */
    @Override
    public void setMapping(String index, String type, String mapping) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        if (StringUtils.isBlank(mapping)) {
            throw new IllegalArgumentException("mapping cannot be blank");
        }

        String[] mappings = mapping.split(COMMA);
        int length = mappings.length;
        if (length % NumberUtils.INTEGER_TWO.intValue() != NumberUtils.INTEGER_ZERO.intValue()) {
            throw new IllegalArgumentException(String.format("mapping must be a string of key, value, the mappings length is [%s]", length));
        }

        try {
            List<SchemaRequest.Update> updates = new ArrayList<>();
            for (int i = NumberUtils.INTEGER_ZERO.intValue(); i < length; i = i + NumberUtils.INTEGER_TWO.intValue()) {
                Map<String, Object> fieldAttributes = new HashMap<>(16);
                fieldAttributes.put(NAME, mappings[i]);
                fieldAttributes.put(TYPE, mappings[i + NumberUtils.INTEGER_ONE.intValue()]);
                updates.add(new SchemaRequest.AddField(fieldAttributes));
            }

            if (updates.isEmpty()) {
                return;
            }

            SchemaRequest.MultiUpdate multiUpdateRequest = new SchemaRequest.MultiUpdate(updates);
            SchemaResponse.UpdateResponse updateResponse = multiUpdateRequest.process(solrClient, index);
            JSONObject responseHeaderObject = this.formatNamedList(updateResponse.getResponseHeader());
            log.info("设置文档类型字段响应信息:[{}]", responseHeaderObject.toJSONString());
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], mapping=[{}]", index, type, mapping, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], mapping=[{}]", index, type, mapping, e);
        }
    }

    @Override
    public List<Map<String, Object>> search(String index, String type, String queryString) {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        if (StringUtils.isBlank(queryString)) {
            throw new IllegalArgumentException("queryString cannot be blank");
        }

        try {
            return searchForMapList(index, (solrQuery) -> {
                List<NameValuePair> pairs = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8);
                for (NameValuePair p : pairs) {
                    solrQuery.set(p.getName(), p.getValue());
                }
            });
        } catch (SolrServerException e) {
            log.error("Solr服务器通信/解析异常, index=[{}], type=[{}], queryString=[{}]", index, type, queryString, e);
        } catch (IOException e) {
            log.error("IO异常, index=[{}], type=[{}], queryString=[{}]", index, type, queryString, e);
        }
        return Collections.emptyList();
    }

    /**
     * 判断索引库中的文档是否已存在
     *
     * @param index 索引库名称
     * @param id    文档ID
     * @return boolean
     * @author heyingcheng
     * @date 2019/7/17 14:13
     */
    private boolean isDocumentExist(String index, String id) throws IOException, SolrServerException {
        Map<String, Object> solrDocument = fetchDocumentById(index, id, null);
        if (MapUtils.isEmpty(solrDocument)) {
            return false;
        }
        return true;
    }

    /**
     * 添加或更新文档
     *
     * @param index          索引库名称
     * @param documentObject 文档对象
     * @return org.apache.solr.client.solrj.response.UpdateResponse
     * @author heyingcheng
     * @date 2019/7/17 14:16
     */
    private UpdateResponse addOrUpdateDocument(String index, Map documentObject) throws SolrServerException, IOException {
        Set<Map.Entry<String, Object>> entries = documentObject.entrySet();
        SolrInputDocument doc = new SolrInputDocument();
        for (Map.Entry<String, Object> entry : entries) {
            doc.setField(entry.getKey(), entry.getValue());
        }
        return solrClient.add(index, doc);
    }

    /**
     * 从索引库中根据ID获取单条文档数据
     *
     * @param index    索引库名称
     * @param id       文档ID
     * @param consumer SolrQueryConsumer
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author heyingcheng
     * @date 2019/7/17 11:49
     */
    private Map<String, Object> fetchDocumentById(String index, String id, Consumer<SolrQuery> consumer) throws IOException, SolrServerException {
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("index cannot be blank");
        }

        SchemaResponse.UniqueKeyResponse response = new SchemaRequest.UniqueKey().process(solrClient, index);
        String uniqueKey = response.getUniqueKey();
        List<Map<String, Object>> documents = searchForMapList(index, (solrQuery) -> {
            solrQuery.set(CommonParams.Q, StringUtils.join(uniqueKey, COLON, id));
            if (Objects.nonNull(consumer)) {
                consumer.accept(solrQuery);
            } else {
                solrQuery.setFields(uniqueKey);
            }
        });
        if (CollectionUtils.isNotEmpty(documents)) {
            return documents.get(NumberUtils.INTEGER_ZERO);
        }
        return Collections.emptyMap();
    }

    /**
     * 按分页方式进行查询
     *
     * <pre>
     *     添加查询条件
     *     添加返回的文档数据字段数组
     *     添加高亮字段
     *     添加排序
     * </pre>
     *
     * @param queryParams 查询参数对象
     * @param startPage   起始页
     * @param pageSize    每页条数
     * @return cms.api.framework.search.SearchResultPage
     * @author heyingcheng
     * @date 2019/7/17 13:49
     */
    private SearchResultPage searchForResultPage(QueryParams queryParams, int startPage, int pageSize) throws SolrServerException, IOException {
        QueryResponse response = searchForQueryResponse(queryParams, startPage, pageSize);
        List<Map<String, Object>> documents = convertQueryResponseToMapList(response);
        SearchResultPage searchResultPage = new SearchResultPage(startPage, pageSize, (int) response.getResults().getNumFound(), documents);
        return searchResultPage;
    }

    /**
     * 按QueryParams进行查询并返回QueryResponse
     *
     * @param queryParams 查询参数对象
     * @param startPage   起始页
     * @param pageSize    每页条数
     * @return org.apache.solr.client.solrj.response.QueryResponse
     * @author heyingcheng
     * @date 2019/7/18 10:53
     */
    private QueryResponse searchForQueryResponse(QueryParams queryParams, int startPage, int pageSize) throws SolrServerException, IOException {
        String index = queryParams.getIndex();
        if (StringUtils.isBlank(index)) {
            throw new IllegalArgumentException("QueryParams.index cannot be blank");
        }

        Map<String, Object> conditions = queryParams.getConditions();
        if (MapUtils.isEmpty(conditions)) {
            throw new IllegalArgumentException("QueryParams.conditions cannot be empty");
        }

        return searchForQueryResponse(index, (solrQuery) -> {
            solrQuery.set(CommonParams.Q, convertConditionsToString(conditions));

            String rtnFields = queryParams.getRtnFields();
            if (StringUtils.isNotBlank(rtnFields)) {
                solrQuery.set(CommonParams.FL, rtnFields);
            }

            String highlightField = queryParams.getHighlightField();
            if (StringUtils.isNotBlank(highlightField)) {
                solrQuery.addHighlightField(highlightField);
            }

            String sortField = queryParams.getSortField();
            if (StringUtils.isNotBlank(sortField)) {
                solrQuery.set(CommonParams.SORT, sortField);
            }

            int start = (startPage - 1) * pageSize;
            solrQuery.setStart(start).setRows(pageSize);
        });
    }

    /**
     * 按SolrQueryConsumer消费者并以列表方式进行查询
     *
     * @param index    索引库名称
     * @param consumer SolrQueryConsumer
     * @return java.util.List<java.util.Map<java.lang.String, java.lang.Object>>
     * @author heyingcheng
     * @date 2019/7/18 10:54
     */
    private List<Map<String, Object>> searchForMapList(String index, Consumer<SolrQuery> consumer) throws IOException, SolrServerException {
        QueryResponse queryResponse = searchForQueryResponse(index, consumer);
        List<Map<String, Object>> documents = convertQueryResponseToMapList(queryResponse);
        return documents;
    }

    /**
     * 按SolrQueryConsumer消费者并以SolrDocumentList方式进行查询
     *
     * @param index    索引库名称
     * @param consumer SolrQueryConsumer
     * @return org.apache.solr.client.solrj.response.QueryResponse
     * @author heyingcheng
     * @date 2019/7/18 10:57
     */
    private QueryResponse searchForQueryResponse(String index, Consumer<SolrQuery> consumer) throws IOException, SolrServerException {
        SolrQuery solrQuery = new SolrQuery();
        consumer.accept(solrQuery);
        QueryResponse queryResponse = solrClient.query(index, solrQuery);
        return queryResponse;
    }

    /**
     * 将查询条件Map转换为Set
     *
     * @param conditions 查询条件
     * @return java.util.Set<java.lang.String>
     * @author heyingcheng
     * @date 2019/7/17 11:48
     */
    private Set<String> convertConditionsToSet(Map<String, Object> conditions) {
        Set<String> querySet = new HashSet<>();
        Set<Map.Entry<String, Object>> entries = conditions.entrySet();
        entries.forEach(entry -> {
            querySet.add(entry.getKey().concat(COLON).concat(String.valueOf(entry.getValue())));
        });
        return querySet;
    }

    /**
     * 将查询条件Map转换为字符串
     *
     * @param conditions 查询条件
     * @return java.lang.String
     * @author heyingcheng
     * @date 2019/7/17 11:48
     */
    private String convertConditionsToString(Map<String, Object> conditions) {
        Set<String> querySet = convertConditionsToSet(conditions);
        return StringUtils.join(querySet, AND);
    }

    /**
     * 将Solr文档集合转换为Map集合
     *
     * @param response 查询响应
     * @return java.util.List<java.util.Map < java.lang.String, java.lang.Object>>
     * @author heyingcheng
     * @date 2019/7/17 11:46
     */
    private List<Map<String, Object>> convertQueryResponseToMapList(QueryResponse response) {
        SolrDocumentList docs = response.getResults();
        if (CollectionUtils.isNotEmpty(docs)) {
            List<Map<String, Object>> documents = new ArrayList<>(docs.size());
            for (SolrDocument d : docs) {
                Map<String, Object> documentMap = convertSolrDocumentToMap(d);
                documents.add(documentMap);
                // 高亮显示的返回结果
                Map<String, Map<String, List<String>>> highlightMap = response.getHighlighting();
                if (MapUtils.isNotEmpty(highlightMap)) {
                    Map<String, List<String>> fieldMap = highlightMap.get(d.get(DEFAULT_DOCUMENT_ID_FIELD_NAME));
                    if (MapUtils.isNotEmpty(fieldMap)) {
                        Set<Map.Entry<String, List<String>>> entries = fieldMap.entrySet();
                        entries.forEach(entry -> {
                            documentMap.put(entry.getKey(), fieldMap.get(entry.getKey()).get(0));
                        });
                    }
                }
            }
            return documents;
        }
        return Collections.emptyList();
    }

    /**
     * 将Solr文档转换为Map，并将子文档对象转换为childDocuments
     *
     * @param solrDocument Solr文档
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author heyingcheng
     * @date 2019/7/17 11:43
     */
    private Map<String, Object> convertSolrDocumentToMap(SolrDocument solrDocument) {
        Map<String, Object> document = new HashMap<>(16);
        Set<Map.Entry<String, Object>> entries = solrDocument.entrySet();
        entries.forEach(entry -> {
            document.put(entry.getKey(), solrDocument.getFirstValue(entry.getKey()));
        });
        if (solrDocument.hasChildDocuments()) {
            List<SolrDocument> childDocuments = solrDocument.getChildDocuments();
            List<Map<String, Object>> childMapDocuments = new ArrayList<>(childDocuments.size());
            for (SolrDocument d : childDocuments) {
                childMapDocuments.add(convertSolrDocumentToMap(d));
            }
            document.put("childDocuments", childMapDocuments);
        }
        return document;
    }

    /**
     * 格式化NamedList
     *
     * @param namedList NamedList
     * @return com.alibaba.fastjson.JSONObject
     * @author heyingcheng
     * @date 2019/6/23 21:14
     */
    protected JSONObject formatNamedList(NamedList namedList) {
        JSONObject jsonObject = new JSONObject();
        int size = namedList.size();
        for (int i = 0; i < size; i++) {
            Object val = namedList.getVal(i);
            if (val instanceof NamedList) {
                jsonObject.put(namedList.getName(i), formatNamedList((NamedList) val));
            } else {
                jsonObject.put(namedList.getName(i), namedList.getVal(i));
            }
        }
        return jsonObject;
    }

}
